import { Participant } from '../helpers/Participant';

import BaseDialog from './BaseDialog';
import BasePageObject from './BasePageObject';

const LOCAL_VIDEO_XPATH = '//span[@id="localVideoContainer"]';
const LOCAL_VIDEO_MENU_TRIGGER = '#local-video-menu-trigger';
const LOCAL_USER_CONTROLS = 'button[title="Local user controls"]';
const HIDE_SELF_VIEW_BUTTON_XPATH = '//div[contains(@class, "popover")]//div[@id="hideselfviewButton"]';

/**
 * Filmstrip elements.
 */
export default class Filmstrip extends BasePageObject {
    /**
     * Asserts that {@code participant} shows or doesn't show the audio
     * mute icon for the conference participant identified by
     * {@code testee}.
     *
     * @param {Participant} testee - The {@code Participant} for whom we're checking the status of audio muted icon.
     * @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon;
     * otherwise, it will assert its presence.
     * @returns {Promise<void>}
     */
    async assertAudioMuteIconIsDisplayed(testee: Participant, reverse = false): Promise<void> {
        let id;

        if (testee === this.participant) {
            id = 'localVideoContainer';
        } else {
            id = `participant_${await testee.getEndpointId()}`;
        }

        const mutedIconXPath
            = `//span[@id='${id}']//span[contains(@id, 'audioMuted')]//*[local-name()='svg' and @id='mic-disabled']`;

        await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
            reverse,
            timeout: 5_000,
            timeoutMsg: `Audio mute icon is${reverse ? '' : ' not'} displayed for ${testee.name}`
        });
    }

    /**
     * Returns the remote display name for an endpoint.
     * @param endpointId The endpoint id.
     */
    async getRemoteDisplayName(endpointId: string) {
        const remoteDisplayName = this.participant.driver.$(`span[id="participant_${endpointId}_name"]`);

        await remoteDisplayName.moveTo();

        return await remoteDisplayName.getText();
    }

    /**
     * Returns the remote video id of a participant with endpointID.
     * @param endpointId
     */
    async getRemoteVideoId(endpointId: string) {
        const remoteDisplayName = this.participant.driver.$(`span[id="participant_${endpointId}"]`);

        await remoteDisplayName.moveTo();

        return await this.participant.execute(eId =>
            document.evaluate(`//span[@id="participant_${eId}"]//video`,
                document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue?.srcObject?.id, endpointId);
    }

    /**
     * Returns the local video id.
     */
    getLocalVideoId() {
        return this.participant.execute(
            'return document.getElementById("localVideo_container").srcObject.id');
    }

    /**
     * Pins a participant by clicking on their thumbnail.
     * @param participant The participant.
     */
    async pinParticipant(participant: Participant) {
        if (participant === this.participant) {
            // when looking up the element and clicking it, it doesn't work if we do it twice in a row (oneOnOne.spec)
            await this.participant.execute(() => document?.getElementById('localVideoContainer')?.click());
        } else {
            const epId = await participant.getEndpointId();

            await this.participant.driver.$(`//span[@id="participant_${epId}"]`).click();
        }
        const endpointID = await participant.getEndpointId();

        await this.participant.waitForParticipantOnLargeVideo(endpointID);
    }

    /**
     * Unpins a participant by clicking on their thumbnail.
     * @param participant
     */
    async unpinParticipant(participant: Participant) {
        const epId = await participant.getEndpointId();

        if (participant === this.participant) {
            await this.participant.execute(() => document?.getElementById('localVideoContainer')?.click());
        } else {
            await this.participant.driver.$(`//span[@id="participant_${epId}"]`).click();
        }

        await this.participant.driver.$(`//div[ @id="pin-indicator-${epId}" ]`).waitForDisplayed({
            timeout: 2_000,
            timeoutMsg: `${this.participant.name} did not unpin ${participant.name}`,
            reverse: true
        });
    }

    /**
     * Gets avatar SRC attribute for the one displayed on small video thumbnail.
     * @param endpointId
     */
    async getAvatar(endpointId: string) {
        const elem = this.participant.driver.$(
            `//span[@id='participant_${endpointId}']//img[contains(@class,'userAvatar')]`);

        return await elem.isExisting() ? await elem.getAttribute('src') : null;
    }

    /**
     * Grants moderator rights to a participant.
     * @param participant
     */
    async grantModerator(participant: Participant) {
        await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'grantmoderatorlink', true);
    }

    /**
     * Clicks on the link in the remote participant actions menu.
     * @param participantId
     * @param linkClassname
     * @param dialogConfirm
     * @private
     */
    private async clickOnRemoteMenuLink(participantId: string, linkClassname: string, dialogConfirm: boolean) {
        await this.participant.driver.$(`//span[@id='participant_${participantId}']`).moveTo();

        await this.participant.driver.$(
            `//span[@id='participant_${participantId
            }']//span[@id='remotevideomenu']//div[@id='remote-video-menu-trigger']`).moveTo();

        const popoverElement = this.participant.driver.$(
            `//div[contains(@class, 'popover')]//div[contains(@class, '${linkClassname}')]`);

        await popoverElement.waitForExist();
        await popoverElement.waitForDisplayed();
        await popoverElement.click();

        if (dialogConfirm) {
            await new BaseDialog(this.participant).clickOkButton();
        }
    }

    /**
     * Mutes the audio of a participant.
     * @param participant
     */
    async muteAudio(participant: Participant) {
        await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'mutelink', false);
    }

    /**
     * Mutes the video of a participant.
     * @param participant
     */
    async muteVideo(participant: Participant) {
        await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'mutevideolink', true);
    }

    /**
     * Kicks a participant.
     * @param participantId
     */
    kickParticipant(participantId: string) {
        return this.clickOnRemoteMenuLink(participantId, 'kicklink', true);
    }

    /**
     * Hover over local video.
     */
    hoverOverLocalVideo() {
        return this.participant.driver.$(LOCAL_VIDEO_MENU_TRIGGER).moveTo();
    }

    /**
     * Clicks on the hide self view button from local video.
     */
    async hideSelfView() {
        // open local video menu
        await this.hoverOverLocalVideo();
        await this.participant.driver.$(LOCAL_USER_CONTROLS).moveTo();

        // click Hide self view button
        const hideSelfViewButton = this.participant.driver.$(HIDE_SELF_VIEW_BUTTON_XPATH);

        await hideSelfViewButton.waitForExist();
        await hideSelfViewButton.waitForClickable();
        await hideSelfViewButton.click();
    }

    /**
     * Checks whether the local self view is displayed or not.
     */
    assertSelfViewIsHidden(hidden: boolean) {
        return this.participant.driver.$(LOCAL_VIDEO_XPATH).waitForDisplayed({
            reverse: hidden,
            timeout: 5000,
            timeoutMsg: `Local video thumbnail is${hidden ? '' : ' not'} displayed for ${this.participant.name}`
        });
    }

    /**
     * Toggles the filmstrip.
     */
    async toggle() {
        const toggleButton = this.participant.driver.$('#toggleFilmstripButton');

        await toggleButton.moveTo();
        await toggleButton.waitForDisplayed();
        await toggleButton.click();
    }

    /**
     * Asserts that the remote videos are hidden or not.
     * @param reverse
     */
    assertRemoteVideosHidden(reverse = false) {
        return this.participant.driver.waitUntil(
            async () =>
                await this.participant.driver.$$('//div[@id="remoteVideos" and contains(@class, "hidden")]').length > 0,
            {
                timeout: 10_000, // 10 seconds
                timeoutMsg: `Timeout waiting fore remote videos to be hidden: ${!reverse}.`
            }
        );
    }

    /**
     * Counts the displayed remote video thumbnails.
     */
    async countVisibleThumbnails() {
        return (await this.participant.driver.$$('//div[@id="remoteVideos"]//span[contains(@class,"videocontainer")]')
            .filter(thumbnail => thumbnail.isDisplayed())).length;
    }

    /**
     * Check if remote videos in filmstrip are visible.
     *
     * @param isDisplayed whether or not filmstrip remote videos should be visible
     */
    verifyRemoteVideosDisplay(isDisplayed: boolean) {
        return this.participant.driver.$('//div[contains(@class, "remote-videos")]/div').waitForDisplayed({
            timeout: 5_000,
            reverse: !isDisplayed,
        });
    }

    /**
     * Checks for visible gaps in the filmstrip thumbnails.
     * This detects if there are any missing thumbnails or excessive spacing between consecutive visible thumbnails.
     *
     * @returns Returns true if gaps are detected, false otherwise.
     */
    async hasGapsInFilmstrip(): Promise<boolean> {
        return await this.participant.execute(() => {
            // Get all visible thumbnail containers in the filmstrip
            const thumbnails = Array.from(
                document.querySelectorAll('#remoteVideos span.videocontainer')
            ).filter((thumb: any) => {
                const style = window.getComputedStyle(thumb);
                const rect = thumb.getBoundingClientRect();

                // Check if element is visible and has dimensions
                return style.display !== 'none'
                    && style.visibility !== 'hidden'
                    && rect.width > 0
                    && rect.height > 0;
            });

            if (thumbnails.length < 2) {
                // Can't have gaps with less than 2 thumbnails
                return false;
            }

            // Get positions and calculated margins of all visible thumbnails
            const positions = thumbnails.map((thumb: any) => {
                const rect = thumb.getBoundingClientRect();
                const style = window.getComputedStyle(thumb);

                return {
                    left: rect.left,
                    right: rect.right,
                    top: rect.top,
                    bottom: rect.bottom,
                    width: rect.width,
                    height: rect.height,
                    marginTop: parseFloat(style.marginTop) || 0,
                    marginBottom: parseFloat(style.marginBottom) || 0,
                    marginLeft: parseFloat(style.marginLeft) || 0,
                    marginRight: parseFloat(style.marginRight) || 0
                };
            });

            // Calculate expected spacing between thumbnails based on first two
            const firstGap = positions.length >= 2
                ? Math.abs(positions[1].top - positions[0].top) !== 0
                    ? positions[1].top - positions[0].bottom // vertical
                    : positions[1].left - positions[0].right // horizontal
                : 0;

            // Check if filmstrip is vertical or horizontal
            const isVertical = Math.abs(positions[1].top - positions[0].top) > Math.abs(positions[1].left - positions[0].left);

            if (isVertical) {
                // For vertical filmstrip, check vertical spacing consistency
                for (let i = 0; i < positions.length - 1; i++) {
                    const current = positions[i];
                    const next = positions[i + 1];
                    const gap = next.top - current.bottom;

                    // Compare against the first gap with some tolerance
                    // Flag if gap is more than 2x the expected spacing
                    if (gap > Math.max(firstGap * 2, current.height * 0.3)) {
                        return true;
                    }
                }
            } else {
                // For horizontal filmstrip, check horizontal spacing consistency
                for (let i = 0; i < positions.length - 1; i++) {
                    const current = positions[i];
                    const next = positions[i + 1];
                    const gap = next.left - current.right;

                    // Compare against the first gap with some tolerance
                    // Flag if gap is more than 2x the expected spacing
                    if (gap > Math.max(firstGap * 2, current.width * 0.3)) {
                        return true;
                    }
                }
            }

            return false;
        });
    }

    /**
     * Asserts that there are no gaps in the filmstrip.
     * This is useful for detecting layout issues where thumbnails might be missing or mispositioned.
     *
     * @param reverse - If true, asserts that gaps should exist. Default false.
     */
    async assertNoGapsInFilmstrip(reverse = false): Promise<void> {
        const hasGaps = await this.hasGapsInFilmstrip();
        const expectedResult = reverse ? true : false;

        if (hasGaps !== expectedResult) {
            throw new Error(
                `Expected filmstrip to ${reverse ? 'have' : 'not have'} gaps, but ${
                    hasGaps ? 'gaps were detected' : 'no gaps were found'
                }`
            );
        }
    }
}
